/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import Long from '../util/long/index'
import Constants from './Constants'
import Util from './Util'

export class BitInputStream {
    constructor() {
    }

    public static isEndOfStream(startAddress: Long, currentAddress: Long, bitsConsumed: number): boolean{
        return startAddress.eq(currentAddress) && bitsConsumed == 64;
    }

    static readTail(inputBase, inputAddress: Long, inputSize: number): Long
    {
        let bits: Long = Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.toNumber()) & 0xFF);

        switch (inputSize) {
            case 7:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(6)
                    .toNumber()) & 0xFF)).shiftLeft(48));
            case 6:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(5)
                    .toNumber()) & 0xFF)).shiftLeft(40));
            case 5:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(4)
                    .toNumber()) & 0xFF)).shiftLeft(32));
            case 4:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(3)
                    .toNumber()) & 0xFF)).shiftLeft(24));
            case 3:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(2)
                    .toNumber()) & 0xFF)).shiftLeft(16));
            case 2:
                bits = bits.or((Long.fromNumber(Unsafe.getByte(inputBase, inputAddress.add(1)
                    .toNumber()) & 0xFF)).shiftLeft(8));
        }
        return bits;
    }

    public static peekBits(bitsConsumed: number, bitContainer: Long, numberOfBits: number): Long {
        let bit: Long = bitContainer.shiftLeft(bitsConsumed).shiftRightUnsigned(1)
        let bits: number = 63 - numberOfBits
        return bit.shiftRightUnsigned(bits);
    }

    public static peekBitsFast(bitsConsumed: number, bitContainer: Long, numberOfBits: number): Long {
        let pbit = bitContainer.shiftLeft(bitsConsumed)
        let pbits = 64 - numberOfBits
        return pbit.shiftRightUnsigned(pbits);
    }
}

export class Initializer {
    private inputBase;
    private startAddress: Long;
    private endAddress: Long;
    private bits: Long;
    private currentAddress: Long;
    private bitsConsumed: number;

    constructor(inputBase: object, startAddress: Long, endAddress: Long) {
        this.inputBase = inputBase;
        this.startAddress = startAddress;
        this.endAddress = endAddress;
    }

    public getBits(): Long
    {
        return this.bits;
    }

    public getCurrentAddress(): Long
    {
        return this.currentAddress;
    }

    public getBitsConsumed(): number
    {
        return this.bitsConsumed;
    }

    public initialize(): void {
        Util.verify((this.endAddress.subtract(this.startAddress)).greaterThanOrEqual(1), this.startAddress, "Bitstream is empty");

        let lastByte: number = Unsafe.getByte(this.inputBase, this.endAddress.subtract(1).toNumber()) & 0xFF;
        Util.verify(lastByte != 0, this.endAddress, "Bitstream end mark not present");

        this.bitsConsumed = Constants.SIZE_OF_LONG - Util.highestBit(lastByte);

        let inputSize: number = this.endAddress.subtract(this.startAddress).toInt();
        if (inputSize >= Constants.SIZE_OF_LONG) { /* normal case */
            this.currentAddress = this.endAddress.subtract(Constants.SIZE_OF_LONG);
            this.bits = Unsafe.getLong(this.inputBase, this.currentAddress.toNumber());
        } else {
            this.currentAddress = this.startAddress;
            this.bits = BitInputStream.readTail(this.inputBase, this.startAddress, inputSize);

            this.bitsConsumed += (Constants.SIZE_OF_LONG - inputSize) * 8;
        }
    }
}

export class Loader {
    private inputBase: any;
    private startAddress: Long;
    private bits: Long;
    private currentAddress: Long;
    private bitsConsumed: number;
    private overflow: boolean;

    constructor(inputBase: object, startAddress: Long, currentAddress: Long, bits: Long, bitsConsumed: number) {
        this.inputBase = inputBase;
        this.startAddress = startAddress;
        this.bits = bits;
        this.currentAddress = currentAddress;
        this.bitsConsumed = bitsConsumed;
    }

    public getBits(): Long
    {
        return this.bits;
    }

    public getCurrentAddress(): Long
    {
        return this.currentAddress;
    }

    public getBitsConsumed(): number
    {
        return this.bitsConsumed;
    }

    public isOverflow(): boolean
    {
        return this.overflow;
    }

    public load(): boolean
    {
        if (this.bitsConsumed > 64) {
            this.overflow = true;
            return true;
        }

        else if (this.currentAddress.eq(this.startAddress)) {
            return true;
        }

        let bytes: number = this.bitsConsumed >>> 3; // divide by 8
        if (this.currentAddress.greaterThanOrEqual(this.startAddress.add(Constants.SIZE_OF_LONG))) {
            if (bytes > 0) {
                this.currentAddress = this.currentAddress.subtract(bytes);
                this.bits = Unsafe.getLong(this.inputBase, this.currentAddress.toNumber());
            }
            this.bitsConsumed &= 0b111;
        }
        else if ((this.currentAddress.subtract(bytes)).lessThan(this.startAddress)) {
            bytes = (this.currentAddress.subtract(this.startAddress)).toInt();
            this.currentAddress = this.startAddress;
            this.bitsConsumed -= bytes * Constants.SIZE_OF_LONG;
            this.bits = Unsafe.getLong(this.inputBase, this.startAddress.toNumber());
            return true;
        }
        else {
            this.currentAddress = this.currentAddress.subtract(bytes);
            this.bitsConsumed -= bytes * Constants.SIZE_OF_LONG;
            this.bits = Unsafe.getLong(this.inputBase, this.currentAddress.toNumber());
        }

        return false;
    }
}